"""
@File    :   service.py
@Time    :   2024/04/06 21:58:37
@Author  :   Qrj
@Email   :   923207736@qq.com
@Description   :   角色相关业务逻辑
"""

from collections import defaultdict

from cashews import NOT_NONE
from sqlalchemy import and_, delete, func, insert, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from src.business.admin.system.menu.models import Menu
from src.business.admin.system.menu.schemas.out import MenuCompleteOutSchema
from src.business.admin.system.user.models import UserRoleRel
from src.exceptions.custom_async_validator import generate_async_validator_exception
from src.schemas.validate import (
    OrderBySearchValidateSchema,
    PaginationSearchValidateSchema,
)
from src.utils.cashews import CashewsCondition, data_cache

from .models import Role, RoleMenuRel
from .schemas.out import ListRoleCompleteOutSchema, RoleCompleteOutSchema
from .schemas.validate import (
    RoleAssignMenuPermissionValidateSchema,
    RoleCreateValidateSchema,
    RoleDeleteValidateSchema,
    RoleListSearchValidateSchema,
    RoleUpdateStatusValidateSchema,
    RoleUpdateValidateSchema,
)


class RoleService:
    """角色业务逻辑类"""

    @data_cache(
        ttl="30m",
        key="role#id:{tenant_id}:{role_id}",
        tags=["role#id:{tenant_id}", "role#id:{tenant_id}:{role_id}"],
        condition=NOT_NONE,
    )
    @staticmethod
    async def get_role_use_id(
        db_session: AsyncSession,
        tenant_id: int,
        role_id: int,
        raise_not_exists_exception: bool = True,
        raise_disabled_exception: bool = True,
    ) -> RoleCompleteOutSchema | None:
        """获取角色信息

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            role_id (int): 角色ID
            raise_not_exists_exception (bool, optional): 未找到时是否报错
            raise_disabled_exception (bool, optional): 禁用时是否报错

        Returns:
            RoleCompleteOutSchema: 角色信息
        """
        search_sql = select(
            *RoleCompleteOutSchema.convert_to_sql_model_field(Role)
        ).where(Role.id == role_id, Role.tenant_id == tenant_id)
        async with db_session.begin():
            search_res = await db_session.execute(search_sql)
        result = search_res.mappings().first()
        if result is not None:
            # 转换成pydantic的序列化模型
            result = RoleCompleteOutSchema.model_validate(result)
            if (not result.status) and raise_disabled_exception:
                # 禁用则报错
                raise generate_async_validator_exception("id", "角色已被禁用")
            return result
        elif result is None and raise_not_exists_exception:
            # 未找到则报错
            raise generate_async_validator_exception("id", "角色不存在")
        else:
            return result

    @data_cache(
        ttl="3m",
        key="complete_role_list:{tenant_id}:{search_schema}:{pagination_schema}:{order_schema}",
        tags=["complete_role_list:{tenant_id}"],
        condition=CashewsCondition.condition_only_true_result,
    )
    @staticmethod
    async def list_search_role(
        db_session: AsyncSession,
        tenant_id: int,
        search_schema: RoleListSearchValidateSchema,
        pagination_schema: PaginationSearchValidateSchema,
        order_schema: OrderBySearchValidateSchema,
        need_select_total: bool = True,
    ) -> tuple[list[RoleCompleteOutSchema], int | None]:
        """完整信息的角色列表查询

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id: 租户ID
            search_schema (MenuListSearchValidateSchema): 查询参数
            pagination_schema: 分页参数
            order_schema: 排序参数
            need_select_total: 是否需要查询总数

        Returns:
            tuple[list[RoleCompleteOutSchema], int | None]: 查询结果、查询总数
        """
        # 构建排序字段
        order_by_field_sql = order_schema.convert_to_order_by_sql(search_schema, Role)
        # 构建查询sql
        list_search_sql = (
            select(*RoleCompleteOutSchema.convert_to_sql_model_field(Role))
            .where(
                and_(*search_schema.convert_to_where_sql(Role)),
                Role.tenant_id == tenant_id,
            )
            .order_by(order_by_field_sql)
            .offset(pagination_schema.offset)
            .limit(pagination_schema.limit)
        )
        # 开始查询
        async with db_session.begin():
            list_search_res = await db_session.execute(list_search_sql)
        result = list_search_res.mappings().all()
        # 根据need_select_total参数来确定是否需要查询总数
        if need_select_total:
            total_sql = (
                select(func.count())
                .select_from(Role)
                .where(
                    and_(*search_schema.convert_to_where_sql(Role)),
                    Role.tenant_id == tenant_id,
                )
            )
            async with db_session.begin():
                total_res = await db_session.execute(total_sql)
            total = total_res.scalar()
        else:
            total = None
        # 统一转为pydantic列表模型
        return ListRoleCompleteOutSchema.validate_python(result), total

    @staticmethod
    async def batch_get_role_menu_info(
        db_session: AsyncSession,
        tenant_id: int,
        *role_id: int,
        need_disabled: bool = False,
    ) -> list[MenuCompleteOutSchema]:
        """批量获取角色的菜单信息列表(内部自动读取和设置缓存)

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            role_id (int): 角色ID
            need_disabled (bool): 是否获取禁用的菜单

        Returns:
            list[MenuCompleteOutSchema]: 菜单信息列表
        """
        # 存储菜单信息的列表
        role_menu_info_list = []
        # 构建缓存key列表
        cache_list = [f"role_menu_info:{tenant_id}:{i}" for i in role_id]
        # 先从缓存中批量获取
        cache_menu_info_list = await data_cache.get_many(*cache_list, default=None)
        # 没有从缓存中找到的菜单权限缓存的角色ID列表
        not_in_cache_role_ids = []
        for cache_menu_info_index, cache_menu_info in enumerate(cache_menu_info_list):
            if cache_menu_info is not None:
                # 添加菜单信息
                [role_menu_info_list.append(i) for i in cache_menu_info]
            else:
                not_in_cache_role_ids.append(role_id[cache_menu_info_index])
        # 没有从缓存中找到的菜单权限缓存的角色, 需要去sql中查找, 并插入缓存
        if not_in_cache_role_ids:
            # 根据need_disabled参数构建join的连接条件
            join_condition = (
                RoleMenuRel.menu_id == Menu.id
                if need_disabled
                else and_(RoleMenuRel.menu_id == Menu.id, Menu.status.is_(True))
            )
            # 从sql中查找not_in_cache_role_ids中的角色对应的菜单信息列表
            not_in_cache_role_ids_sql = (
                select(
                    RoleMenuRel.role_id,
                    *MenuCompleteOutSchema.convert_to_sql_model_field(Menu),
                )
                .select_from(RoleMenuRel)
                .join(Menu, join_condition)
                .where(
                    RoleMenuRel.role_id.in_(not_in_cache_role_ids),
                    RoleMenuRel.tenant_id == tenant_id,
                )
            )
            async with db_session.begin():
                not_in_cache_role_ids_res = await db_session.execute(
                    not_in_cache_role_ids_sql
                )
            not_in_cache_role_ids_result = not_in_cache_role_ids_res.mappings().all()
            # 将sql查询结果转为角色与其菜单信息标识集合的映射
            not_in_cache_role_menu_info_mapping = defaultdict(list)
            for not_in_cache_role_menu_info in not_in_cache_role_ids_result:
                # 单独解析出菜单信息
                not_in_cache_role_menu_info_mapping[
                    not_in_cache_role_menu_info["role_id"]
                ].append(
                    MenuCompleteOutSchema.model_validate(not_in_cache_role_menu_info)
                )
            # 将角色的菜单信息插入缓存
            for (
                k_role_id,
                v_menu_info_list,
            ) in not_in_cache_role_menu_info_mapping.items():
                await data_cache.set(
                    f"role_menu_info:{tenant_id}:{k_role_id}:{need_disabled}",
                    v_menu_info_list,
                    expire="30m",
                    tags=[
                        "role_menu_info",
                        f"role_menu_info:{tenant_id}",
                        f"role_menu_info:{tenant_id}:{k_role_id}",
                    ],
                )
        # 将菜单信息去重
        result = []
        [result.append(i) for i in role_menu_info_list if i not in result]
        return result

    @staticmethod
    async def batch_get_role_menu_id(
        db_session: AsyncSession,
        tenant_id: int,
        *role_id: int,
        need_disabled: bool = False,
    ) -> list[int]:
        """批量获取角色的菜单id列表(内部自动读取和设置缓存)
        用于读取用户可见的菜单

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            role_id (int): 角色ID
            need_disabled (bool): 是否获取禁用的菜单

        Returns:
            list[int]: 角色的菜单ID列表
        """
        # 批量获取角色的菜单信息列表
        role_menu_info_list = await RoleService.batch_get_role_menu_info(
            db_session, tenant_id, *role_id, need_disabled=need_disabled
        )
        return [i.id for i in role_menu_info_list]

    @staticmethod
    async def batch_get_role_menu_permission(
        db_session: AsyncSession,
        tenant_id: int,
        *role_id: int,
        need_disabled: bool = False,
    ) -> list[str]:
        """批量获取角色的菜单权限标识列表(内部自动读取和设置缓存)
        用于验证用户菜单权限

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            role_id (int): 角色ID
            need_disabled (bool): 是否获取禁用的菜单

        Returns:
            list[str]: 角色的菜单权限标识列表
        """
        # 批量获取角色的菜单信息列表
        role_menu_info_list = await RoleService.batch_get_role_menu_info(
            db_session, tenant_id, *role_id, need_disabled=need_disabled
        )
        return [i.permission for i in role_menu_info_list if i.permission != ""]

    @staticmethod
    @data_cache.transaction()
    async def create_role(
        db_session: AsyncSession,
        tenant_id: int,
        create_schema: RoleCreateValidateSchema,
        creator_username: str = "",
    ) -> int:
        """创建角色

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            create_user_info (UserCreateValidateSchema): 创建用户的信息
            creator_username: 创建者用户名
            is_admin: 是否是管理员角色

        Returns:
            int: 创建的sql的主键ID
        """
        # 构建用户的创建信息字典(排除password参数)
        create_info_dict = create_schema.model_dump()
        # 创建
        create_sql = insert(Role).values(
            tenant_id=tenant_id,
            creator_username=creator_username,
            **create_info_dict,
        )
        async with db_session.begin():
            create_res = await db_session.execute(create_sql)
            # 清空相关缓存标签
            await data_cache.delete_tags(
                f"complete_role_list:{tenant_id}",
            )
        return create_res.inserted_primary_key_rows[0][0]

    @staticmethod
    @data_cache.transaction()
    async def update_role(
        db_session: AsyncSession,
        tenant_id: int,
        update_schema: RoleUpdateValidateSchema,
        updater_username: str = "",
    ):
        """修改角色信息

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            update_schema (RoleUpdateValidateSchema): 修改操作所需的数据
            updater_username (str, optional):修改者用户名
        """
        # 检查需修改的sql实例是否存在(禁用不报错)
        await RoleService.get_role_use_id(
            db_session, tenant_id, update_schema.id, raise_disabled_exception=False
        )
        # 构建修改所需的数据(排除未设置的值和id)
        update_schema_data = update_schema.model_dump(
            exclude=("id",), exclude_unset=True
        )
        if update_schema_data:
            # 修改sql
            update_sql = (
                update(Role)
                .values(**update_schema_data, updater_username=updater_username)
                .where(Role.id == update_schema.id, Role.tenant_id == tenant_id)
            )
            # 修改操作
            async with db_session.begin():
                # 修改sql数据
                await db_session.execute(update_sql)
                # 清空相关缓存标签
                await data_cache.delete_tags(
                    f"role#id:{tenant_id}:{update_schema.id}",
                    f"complete_role_list:{tenant_id}",
                )

    @staticmethod
    @data_cache.transaction()
    async def update_role_status(
        db_session: AsyncSession,
        tenant_id: int,
        update_schema: RoleUpdateStatusValidateSchema,
        updater_username: str = "",
    ):
        """单独修改角色状态

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            update_schema (RoleUpdateValidateSchema): 修改操作所需的数据
            updater_username (str, optional):修改者用户名
        """
        # 检查需修改的sql实例是否存在(禁用不报错)
        await RoleService.get_role_use_id(
            db_session, tenant_id, update_schema.id, raise_disabled_exception=False
        )
        # 修改sql
        update_sql = (
            update(Role)
            .values(status=update_schema.status, updater_username=updater_username)
            .where(Role.id == update_schema.id, Role.tenant_id == tenant_id)
        )
        # 修改操作
        async with db_session.begin():
            # 修改sql数据
            await db_session.execute(update_sql)
            # 清空相关缓存标签
            await data_cache.delete_tags(
                f"role#id:{tenant_id}:{update_schema.id}",
                f"complete_role_list:{tenant_id}",
            )

    @staticmethod
    @data_cache.transaction()
    async def assign_role_menu_permission(
        db_session: AsyncSession,
        tenant_id: int,
        assign_schema: RoleAssignMenuPermissionValidateSchema,
    ):
        """分配角色菜单权限

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id (int): 租户ID
            assign_schema (RoleAssignMenuPermissionValidateSchema): 分配操作所需的数据
        """
        # 查找需分配的sql实例(禁用不报错)
        await RoleService.get_role_use_id(
            db_session, tenant_id, assign_schema.id, raise_disabled_exception=False
        )
        # 只留下实际存在的菜单ID
        if assign_schema.menu_ids:
            # 菜单ID只能在租户所拥有的菜单列表范围内
            from src.business.admin.system.tenant.package.service import (
                TenantPackageService,
            )
            from src.business.admin.system.tenant.tenant.service import TenantService

            # 获取租户所拥有的菜单列表
            tenant = await TenantService.get_tenant_use_id(db_session, tenant_id)
            if tenant.is_system:
                all_menu_ids_search_sql = select(Menu.id)
                async with db_session.begin():
                    all_menu_ids_search_res = await db_session.execute(
                        all_menu_ids_search_sql
                    )
                allow_insert_menu_ids = all_menu_ids_search_res.scalars().all()
            else:
                tenant_package = await TenantPackageService.get_tenant_package_use_id(
                    db_session, tenant.tenant_package_id
                )
                # 构建出允许添加的菜单ID列表
                allow_insert_menu_ids = [
                    i for i in assign_schema.menu_ids if i in tenant_package.menu_ids
                ]
            # 查询出实际存在的菜单ID
            exists_menu_ids_sql = select(Menu.id).where(
                Menu.id.in_(allow_insert_menu_ids)
            )
            async with db_session.begin():
                exists_menu_ids_res = await db_session.execute(exists_menu_ids_sql)
            exists_menu_ids = exists_menu_ids_res.scalars().all()
        else:
            exists_menu_ids = []

        # 修改中间表(先删再添加)
        # 删除的sql
        delete_sql = delete(RoleMenuRel).where(
            RoleMenuRel.role_id == assign_schema.id,
            RoleMenuRel.tenant_id == tenant_id,
        )
        # 批量新增的sql
        insert_batch_dict_list = [
            {"tenant_id": tenant_id, "role_id": assign_schema.id, "menu_id": menu_id}
            for menu_id in exists_menu_ids
        ]
        insert_sql = (insert(RoleMenuRel), insert_batch_dict_list)
        # 整体操作
        async with db_session.begin():
            # 删除操作
            await db_session.execute(delete_sql)
            # 批量新增操作(非空才需要添加)
            if insert_batch_dict_list:
                await db_session.execute(*insert_sql)
            # 清空相关缓存标签
            await data_cache.delete_tags(
                f"role_menu_info:{tenant_id}:{assign_schema.id}"
            )

    @staticmethod
    @data_cache.transaction()
    async def delete_role(
        db_session: AsyncSession,
        tenant_id: int,
        delete_schema: RoleDeleteValidateSchema,
    ):
        """删除角色

        Args:
            db_session (AsyncSession): 数据库连接池
            tenant_id: 租户ID
            delete_schema (RoleDeleteValidateSchema): 删除操作所属的数据
        """
        # 查找需删除的角色
        # 构建查询sql
        select_sql = select(Role.id, Role.is_admin).where(
            Role.id.in_(delete_schema.ids), Role.tenant_id == tenant_id
        )
        # 进行sql查询
        async with db_session.begin():
            select_res = await db_session.execute(select_sql)
        select_result = select_res.mappings().all()
        # 检查是否删除了管理员级别角色
        if any([i["is_admin"] for i in select_result]):
            raise generate_async_validator_exception("ids", "不允许删除管理员级别用户")

        # 进行删除操作
        if select_result:
            # 获取需要删除的角色id列表
            delete_id_list = [i["id"] for i in select_result]
            # 删除角色本身的sql
            delete_sql = delete(Role).where(Role.id.in_(delete_id_list))
            # 删除角色的相关菜单权限sql
            delete_role_permission_rel_sql = delete(RoleMenuRel).where(
                RoleMenuRel.role_id.in_(delete_id_list)
            )
            # 删除用户相关的角色
            delete_user_role_rel_sql = delete(UserRoleRel).where(
                UserRoleRel.role_id.in_(delete_id_list)
            )
            # 删除操作
            async with db_session.begin():
                # 删除sql中的数据
                await db_session.execute(delete_sql)
                await db_session.execute(delete_role_permission_rel_sql)
                await db_session.execute(delete_user_role_rel_sql)
                delete_cache_tags = [
                    *[f"role#id:{tenant_id}:{i}" for i in delete_id_list],
                    *[f"role_menu_info:{tenant_id}:{i}" for i in delete_id_list],
                    f"user_role_ids:{tenant_id}",
                    f"complete_role_list:{tenant_id}",
                ]
                # 清空相关缓存标签
                await data_cache.delete_tags(*delete_cache_tags)
